## {{{ http://code.activestate.com/recipes/534131/ (r1)
"""pypopper: a file-based pop3 server

Useage:
	python pypopper.py <port> <path_to_message_file>
"""
import logging
import os
import socket
import sys
import traceback
import hashlib
import time

from subprocess import Popen, PIPE

if not sys.argv[1:]: 
	sys.argv += ["110", "data"]

logging.basicConfig(format="%(name)s %(levelname)s - %(message)s")
log = logging.getLogger("pypopper")
log.setLevel(logging.DEBUG)

CRLF = "\r\n"

class ProgressBar:
    def __init__(self):
		try:
			self.width = int(Popen(['tput', 'cols'], stdout=PIPE).communicate()[0]) - 11
		except:
			self.width = 60

    def printProgress(self, progressValue):
        done = int(self.width * progressValue + 0.5)
        percentStr = "%6.2f%%" % (progressValue * 100.0)
        print "\r[" + "#" * done + " " * (self.width - done) + "] " + percentStr,
        sys.stdout.flush()

class ChatterboxConnection(object):
	def __init__(self, conn):
		self.conn = conn
	def __getattr__(self, name):
		return getattr(self.conn, name)

	def sendall(self, data, CRLF=CRLF):
		if data is None:
			return

		if len(data) < 50:
			log.debug("send: %r", data)
		else:
			log.debug("send: %r...", data[:50])
		data += CRLF
		self.conn.sendall(data)

	def recvall(self, CRLF = CRLF):
		data = []
		i = 0
		while True:
			chunk = self.conn.recv(4096)

			print "chunk:", i, len(chunk), chunk

			if CRLF in chunk:
				data.append(chunk[:chunk.index(CRLF)])
				break

			data.append(chunk)

			if len(data) > 1:
				pair = data[-2] + data[-1]
				if CRLF in pair:
					data[-2] = pair[:pair.index(CRLF)]
					data.pop()
					break

			if chunk is None or len(chunk) == 0:
				print "WE ARE BREAKING"
				break

			if i > 10:
				print "Sick!!!"
				break

		log.debug("recv: %r", "".join(data))
		return "".join(data)

class Message():
	def __init__(self):
		self.fileName = ""
		self.byteOffset = 0
		self.length = 0
		self.uid = ""

class MbsDb():
	def __init__(self, fileName):
		self.db = dict()
		self.counter = 0
		self.size = 0
		self.fileName = fileName
		self.fileList = []
		self.findAllFiles(fileName)

		i = 0
		numFiles = float(len(self.fileList))
		tb = ProgressBar()

		print
		print "Indexing mail database"
		for f in self.fileList:
			i += 1
			tb.printProgress(i / numFiles)
			self.parseFile(f)

		print
		print len(self.db), "messages found"
		print

	def findAllFiles(self, fileName):
		names = os.listdir(fileName)
		for name in names:
			srcname = os.path.join(fileName, name)
			if os.path.isdir(srcname):
				self.findAllFiles(srcname)
			else:
				#print "FILE: ", srcname
				self.fileList.append(srcname)

	def isStartOfMessage(self, data, fromLineStart):
		"""fromLine start is the position of the where the message's start position is,
		i.e. where the From-line starts."""
		fromLineEnd = data.find("\n", fromLineStart)
		if fromLineEnd == -1:
			return 50 * "Z"
			return False

		nextLineEnd = data.find("\n", fromLineEnd+1)
		if nextLineEnd == -1:
			return 50 * "X"
			return False

		nextLine = data[fromLineEnd+1:nextLineEnd-1]
		return "X-Opera-Status" in nextLine

	def findFromLine(self, data, startPos):
		while True:
			fromLineStart = data.find("\nFrom ", startPos)

			if fromLineStart == -1:
				return -1

			fromLineStart += 1  # point to the 'F' in "\nFrom"

			if self.isStartOfMessage(data, fromLineStart):
				return fromLineStart
			else:
				startPos = fromLineStart

	def parseFile(self, fileName):
		msg = open(fileName, "rb")
		
		try:
			data = msg.read()
			
			# if file is empty
			if len(data) == 0:
				return;

			if data.find("From ", 0) != 0:
				log.warning("%s does not begin with 'From '", fileName)
				return

			messageStart = 0
			messageEnd = 0

			while messageEnd != len(data):
				messageEnd = self.findFromLine(data, messageStart)

				if messageEnd == -1:
					messageEnd = len(data) # If not found use end of file

				m = Message()
				m.fileName = fileName
				m.byteOffset = messageStart
				m.length = messageEnd - messageStart
				m.uid = hashlib.sha224(data[messageStart:messageEnd]).hexdigest()

				self.db[len(self.db)] = m
				self.size += m.length

				messageStart = messageEnd

		finally:
			msg.close()

	def getMessage(self, id):
		message = self.db[id]
		f = open(message.fileName, "rb")
		content = None
		try:
			f.seek(message.byteOffset)
			content = f.read(message.length)
		finally:
			f.close()

		return content


def handleUser(data, msg):
	return "+OK user accepted"

def handlePass(data, msg):
	return "+OK pass accepted"

def handleStat(data, msg):
	return "+OK " + str(len(msg.db)) + " " + str(msg.size)

def handleList(data, msg):
	s = "+OK " + str(len(msg.db)) + " messages (" + str(msg.size) + " octets)" + CRLF
	for m in msg.db.iteritems():
		key, value = m
		s += str(key + 1) + " " + str(value.length) + CRLF
	s += "."
	return s

def handleTop(data, msg):
	if len(data.split(None, 2)) < 3:
		return "-ERR Error"


	cmd, msgIdStr, linesStr = data.split()
	
	lines = int(linesStr)
	msgId = int(msgIdStr) - 1
	if (msgId >= 0) and (msgId < len(msg.db)):
		m = msg.getMessage(msgId)
		s = "+OK top of message follows" + CRLF

		body = False
		lineCounter = 0
		for line in m.split(CRLF):
			if body:
				lineCounter += 1

			if line is "":
				body = True
		
			if line == ".":
				line = ".."

			s += line + CRLF

			if lineCounter == lines:
				break

		s += "."

		return s
	else:
		return "-ERR no such message, only " + str(len(msg.db)) + " message in maildrop"

def handleRetr(data, msg):
	if len(data.split(None, 1)) < 2:
		return "-ERR Argument error"

	arg = data.split(None, 1)[1]

	msgId = int(arg) - 1
	if (msgId >= 0) and (msgId < len(msg.db)):
		m = msg.getMessage(msgId)
		s = "+OK " + str(len(m)) + " octets" + CRLF
		for line in m.split(CRLF):
			if line == ".":
				line = ".."

			s += line + CRLF
		
		s += "."

		return s

	return "-ERR no such message, only " + str(len(msg.db)) + " message in maildrop"

def handleDele(data, msg):
	return "+OK message 1 deleted (Just fake)"

def handleNoop(data, msg):
	return "+OK"

def handleQuit(data, msg):
	return "+OK pypopper POP3 server signing off"

def handleUidl(data, msg):
	if len(data.split(None, 1)) == 1:
		# No argument, so list all
		s = "+OK" + CRLF
		for m in msg.db.iteritems():
			key, value = m
			s += str(key + 1) + " " + value.uid + CRLF
		s += "."
		return s
	
	# We have an argument
	arg = data.split(None, 1)[1]
	msgId = int(arg) - 1
	if (msgId >= 0) and (msgId < len(msg.db)):
		return "+OK " + str(msgId) + " " + msg.db[msgId].uid
	
	return "-ERR no such message, only " + str(len(msg.db)) + " message in maildrop"

def handleCapa(data, msg):
	return "+OK Capability list follows" + CRLF + "USER" + CRLF + "UIDL" + CRLF + "TOP" + CRLF + "."

dispatch = dict(
	USER=handleUser,
	PASS=handlePass,
	STAT=handleStat,
	LIST=handleList,
	TOP=handleTop,
	RETR=handleRetr,
	DELE=handleDele,
	NOOP=handleNoop,
	QUIT=handleQuit,
	UIDL=handleUidl,
	CAPA=handleCapa
)

def	serve(host,	port, db):
	sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	sock.bind((host, port))
	try:
		if host:
			hostname = host
		else:
			hostname = "localhost"

		log.info("pypopper POP3 serving '%s' on %s:%s", db.fileName, hostname, port)

		while True:
			sock.listen(1)
			conn, addr = sock.accept()
			log.debug('Connected by	%s', addr)

			try:
				conn = ChatterboxConnection(conn)
				conn.sendall("+OK pypopper file-based pop3 server ready")

				while True:
					data = conn.recvall()

					if data == "" or len(data) == 0:
						conn.sendall("-ERR unknown command")
						time.sleep(1)
						continue
						
					command = data.split(None, 1)[0]

					try:
						cmd = dispatch[command]
					except KeyError:
						conn.sendall("-ERR unknown command")
					else:
						conn.sendall(cmd(data, db))
						if cmd is handleQuit:
							break
			finally:
				conn.close()
				#msg = None
	except (SystemExit,	KeyboardInterrupt):
		log.info("pypopper stopped")
	except Exception, ex:
		log.critical("fatal error", exc_info=ex)
	finally:
		sock.shutdown(socket.SHUT_RDWR)
		sock.close()

def main():
	if len(sys.argv) !=	3:
		print("USAGE: [<host>:]<port> <path_to_message_file>")
	else:
		_, port, filename = sys.argv
		if ":" in port:
			host = port[:port.index(":")]
			port = port[port.index(":") + 1:]
		else:
			host = ""

		try:
			port = int(port)
		except Exception:
			print("Unknown port:", port)
		else:
			if os.path.exists(filename):
				db = MbsDb(filename)
				serve(host,	port, db)
			else:
				print("File	not	found:", filename)

if __name__ == "__main__":
	main()
	

## end of http://code.activestate.com/recipes/534131/ }}}
